import { useRouter } from "next/router"; import { api } from "@/src/utils/api"; import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess"; import { Card, CardContent, CardHeader, CardTitle, } from "@/src/components/ui/card"; import { DeleteModelButton } from "@/src/features/models/components/DeleteModelButton"; import { EditModelButton } from "@/src/features/models/components/EditModelButton"; import { CloneModelButton } from "@/src/features/models/components/CloneModelButton"; import { TestModelMatchButton } from "@/src/features/models/components/test-match/TestModelMatchButton"; import { JSONView } from "@/src/components/ui/CodeJsonViewer"; import Link from "next/link"; import { Button } from "@/src/components/ui/button"; import { getMaxDecimals } from "@/src/features/models/utils"; import Decimal from "decimal.js"; import { PriceUnitSelector } from "@/src/features/models/components/PriceUnitSelector"; import { useMemo, useState } from "react"; import { usePriceUnitMultiplier } from "@/src/features/models/hooks/usePriceUnitMultiplier"; import Generations from "@/src/components/table/use-cases/observations"; import Page from "@/src/components/layouts/page"; import { SquareArrowOutUpRight, Info as InfoIcon } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/src/components/ui/hover-card"; import { CodeMirrorEditor } from "@/src/components/editor"; import { useEffect } from "react"; export default function ModelDetailPage() { const router = useRouter(); const { priceUnit, priceUnitMultiplier } = usePriceUnitMultiplier(); const projectId = router.query.projectId as string; const modelId = router.query.modelId as string; const pricingTierParam = router.query.pricingTier as string | undefined; const hasWriteAccess = useHasProjectAccess({ projectId, scope: "models:CUD", }); const { data: model, isLoading } = api.models.getById.useQuery( { projectId, modelId }, { enabled: !!projectId && !!modelId }, ); // Get default tier or first tier by priority const defaultTier = useMemo(() => { if (!model?.pricingTiers || model.pricingTiers.length === 0) return null; return model.pricingTiers.find((t) => t.isDefault) || model.pricingTiers[0]; }, [model?.pricingTiers]); // State for selected pricing tier - initialize from URL param const [selectedTierId, setSelectedTierId] = useState( pricingTierParam ?? null, ); // Sync with URL parameter when it changes useEffect(() => { if (pricingTierParam && model?.pricingTiers) { const tierExists = model.pricingTiers.some( (t) => t.id === pricingTierParam, ); if (tierExists) { setSelectedTierId(pricingTierParam); } } }, [pricingTierParam, model?.pricingTiers]); // Get the active tier (selected or default) const activeTier = useMemo(() => { if (!model?.pricingTiers) return null; if (selectedTierId) { return model.pricingTiers.find((t) => t.id === selectedTierId) || null; } return defaultTier; }, [model?.pricingTiers, selectedTierId, defaultTier]); const maxDecimals = useMemo( () => Math.max( ...Object.values(activeTier?.prices ?? {}).map((price) => getMaxDecimals(price, priceUnitMultiplier), ), ), [activeTier?.prices, priceUnitMultiplier], ); // If not found, redirect to models page if (!isLoading && !model) { return (
Model not found
); } const isLangfuseModel = !Boolean(model?.projectId); if (isLoading || !model) { return
Loading...
; } return ( {hasWriteAccess && ( <> {!isLangfuseModel ? ( <> { void router.push( `/project/${projectId}/settings/models`, ); }} /> ) : ( )} )} ), }} >
Model configuration
Match Pattern
{model.matchPattern}
Maintained by
{isLangfuseModel ? "Langfuse" : "User"}
Tokenizer
{model.tokenizerId || "None"}
{model.tokenizerId && (
Tokenizer Config
                  
                
)}
Pricing {model.pricingTiers.length > 1 && (
{activeTier && !activeTier.isDefault && (

Pricing Tier Conditions

This tier is applied when the following conditions are met:

{}} // Read-only minHeight="none" className="max-h-[250px] overflow-y-auto" editable={false} />
)}
)}
Usage Type Price {priceUnit}
{activeTier && Object.entries(activeTier.prices) // Sort by price ascending .sort((a, b) => a[1] - b[1]) .map(([usageType, price]) => (
{usageType} $ {new Decimal(price) .mul(priceUnitMultiplier) .toFixed(maxDecimals)}
))}
Model observations
); }